﻿// MonoGame - Copyright (C) MonoGame Foundation, Inc
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace MonoGame.Effect.Compiler
{
    public static class Program
    {
        private static readonly List<(string, string)> sourceFiles = new();

        private static readonly Regex lineColumnRegex = new(@"\((\d*)(,)?(\d*)?(-)?(\d*)?\)", RegexOptions.Compiled);

        public static int Main(string[] args)
        {
            if (!Environment.Is64BitProcess)
            {
                Console.Error.WriteLine("The MonoGame content tools only work on a 64bit OS.");
                return -1;
            }

            var options = new Options();
            var parser = new CommandLineParser(options);
            parser.Title = "mgfxc - The MonoGame Effect compiler.";

            if (!parser.ParseCommandLine(args))
                return 1;

            var sourceFilepath = Path.GetFullPath(options.SourceFile);

            var nativeSourceFilepath = ConvertToNative(sourceFilepath);

            sourceFiles.Add((sourceFilepath, nativeSourceFilepath));

            var sourceDirectory = Path.GetDirectoryName(sourceFilepath);
            sourceFiles.Add((sourceDirectory, ConvertToNative(sourceDirectory)));

            // Validate the input file exits.
            if (!File.Exists(sourceFilepath))
            {
                Console.Error.WriteLine("The input file '{0}' was not found!", nativeSourceFilepath);
                return 1;
            }

            // TODO: This would be where we would decide the user
            // is trying to convert an FX file to a MGFX glsl file.
            //
            // For now we assume we're going right to a compiled MGFXO file.

            // Parse the MGFX file expanding includes, macros, and returning the techniques.
            ShaderResult shaderResult;
            try
            {
                shaderResult = ShaderResult.FromFile(sourceFilepath, options, new ConsoleEffectCompilerOutput());
                shaderResult.RelativeFilePath = options.SourceFile;

                foreach (var dependency in shaderResult.Dependencies)
                {
                    var dependencyNativeFilename = ConvertToNative(dependency);
                    Console.WriteLine("Dependency: " + dependencyNativeFilename);

                    sourceFiles.Add((dependency, dependencyNativeFilename));
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ConvertMessage(ex.Message));
                Console.Error.WriteLine("Failed to parse '{0}'!", nativeSourceFilepath);
                return 1;
            }

            // Create the effect object.
            EffectObject effect;
            var shaderErrorsAndWarnings = string.Empty;
            try
            {
                effect = EffectObject.CompileEffect(shaderResult, out shaderErrorsAndWarnings);

                if (!string.IsNullOrEmpty(shaderErrorsAndWarnings))
                    Console.Error.WriteLine(ConvertMessage(shaderErrorsAndWarnings));
            }
            catch (ShaderCompilerException)
            {
                // Write the compiler errors and warnings and let the user know what happened.
                Console.Error.WriteLine(ConvertMessage(shaderErrorsAndWarnings));
                Console.Error.WriteLine("Failed to compile '{0}'!", nativeSourceFilepath);
                return 1;
            }
            catch (Exception ex)
            {
                // First write all the compiler errors and warnings.
                if (!string.IsNullOrEmpty(shaderErrorsAndWarnings))
                    Console.Error.WriteLine(ConvertMessage(shaderErrorsAndWarnings));

                // If we have an exception message then write that.
                if (!string.IsNullOrEmpty(ex.Message))
                    Console.Error.WriteLine(ConvertMessage(ex.Message));

                // Let the user know what happened.
                Console.Error.WriteLine("Unexpected error compiling '{0}'!", nativeSourceFilepath);
                return 1;
            }

            // Get the output file path.
            if (options.OutputFile == string.Empty)
                options.OutputFile = Path.GetFileNameWithoutExtension(sourceFilepath) + ".mgfxo";

            var outputFilepath = Path.GetFullPath(options.OutputFile);
            var nativeOutputFilepath = ConvertToNative(outputFilepath);

            // Write out the effect to a runtime format.
            try
            {
                bool writeHeader = outputFilepath.EndsWith(".h", StringComparison.OrdinalIgnoreCase);
                if (writeHeader)
                {
                    using (var stream = new FileStream(outputFilepath, FileMode.Create, FileAccess.Write))
                    using (var binary = new MemoryStream())
                    {
                        // Write the shader to the binary file.                        
                        using (var writer = new BinaryWriter(binary))
                            effect.Write(writer, options);

                        var code = new StringBuilder();
                        var data = binary.ToArray();
                        for (int i = 0; i < data.Length; i++)
                        {
                            if (i % 12 == 0)
                                code.Append($"{Environment.NewLine}\t");

                            code.Append("0x");
                            code.Append(data[i].ToString("x2"));
                            code.Append(", ");
                        }

                        // Remove the trailing ", "
                        code.Remove(code.Length - 2, 2);

                         var symbol = Path.GetFileName(outputFilepath);
                        symbol = symbol.Substring(0, symbol.Length - 2);
                        symbol = symbol.Replace('.', '_');

                        var guard = $"{symbol.ToUpper()}_H";

                        // Write the header.
                        using (var text = new StreamWriter(stream))
                        {
                            text.WriteLine($$"""
                                    // This code was auto generated by the MonoGame Effect Compiler.
                                    // Do not modify it by hand. Use MGFXC.EXE to regenerate it.
                                    //
                                    // Source: {{Path.GetFileName(sourceFilepath)}}
                                    // Profile: {{options.Profile.Name}}
                                    // Defines: {{options.Defines}}
                                    // Debug: {{options.Debug}}
                                    // Size: {{data.Length}} bytes
                                    // Version: {{EffectObject.Version}}

                                    #ifndef {{guard}}
                                    #define {{guard}}

                                    static const unsigned char {{symbol}}[] = {{{code}}
                                    };

                                    #endif // {{guard}}
                                    """);
                        }
                    }
                }
                else
                {
                    using (var stream = new FileStream(outputFilepath, FileMode.Create, FileAccess.Write))
                    using (var writer = new BinaryWriter(stream))
                        effect.Write(writer, options);
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ConvertMessage(ex.Message));
                Console.Error.WriteLine("Failed to write '{0}'!", nativeOutputFilepath);
                return 1;
            }

            // We finished succesfully.
            Console.WriteLine("Compiled '{0}' to '{1}'.", nativeSourceFilepath, nativeOutputFilepath);
            return 0;
        }

        private static string ConvertToNative(string path)
        {
            return path.Replace('\\', '/');
        }

        private static string ConvertMessage(string sourceString)
        {
            sourceString = sourceString.Replace("\\\\", "\\");

            foreach (var (originalFilename, newFilename) in sourceFiles)
                sourceString = sourceString.Replace(originalFilename, newFilename);

            sourceString = lineColumnRegex.Replace(sourceString, ReplaceRowAndColumn);

            return sourceString;
        }

        private static string ReplaceRowAndColumn(Match match)
        {
            var groups = match.Groups;
            if (groups.Count != 6)
                return match.Value;

            var result = $"({groups[1]}," + (groups[2].Success && groups[3].Value != "0" ? groups[3] : "1");
            if (groups[4].Success)
                result += $",{groups[1]},{int.Parse(groups[5].ValueSpan) + 1}";
            result += ")";

            return result;
        }

        private class ConsoleEffectCompilerOutput : IEffectCompilerOutput
        {
            public void WriteWarning(string file, int line, int column, string message)
            {
                Console.Error.WriteLine("{0}({1},{2}): warning PREPROCESS01: {3}", ConvertToNative(file), line, column, ConvertMessage(message));
            }

            public void WriteError(string file, int line, int column, string message)
            {
                throw new Exception(string.Format("{0}({1},{2}): error PREPROCESS01: {3}", ConvertToNative(file), line, column, ConvertMessage(message)));
            }
        }
    }
}
